package smtp;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Flags.Flag;
import javax.mail.event.MessageCountEvent;
import javax.mail.event.MessageCountListener;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;

/**
 * This class maintains access to an IMAPFolder and models its
 * data (i.e. IMAP messages) for a list.  It creates a view of a
 * "current page" of messages.  The messages are displayed in
 * chronological order with the newest appearing first.
 * 
 * Next and prev methods may be used to page through the list.
 * 
 * This class also functions as a cache, keeping track of which
 * messages have been requested and which are already requested
 * so not to re-request them. (This is may be unnecessary as
 * underlying classes also keep a cache.)
 * 
 * @author chandlerr, liw, wendlandts
 *
 */
public class MessageListModel implements ListModel {
	// initial # of items per page may be modified here
	private static int ITEMS_PER_PAGE = 15;

	// the folder containing messages to display
	IMAPFolder folder;

	// Actions to be called when the use wants to mark unread or delete messages
	// Not implemented
	ActionListener expungeAction = null;
	ActionListener markUnseenAction = null;

	// Listeners for changes to the available list data
	LinkedList<ListDataListener> dataListeners;
	
	// The downloaded messages
	// THE FIRST PAGE STARTS AT THE END OF THE ARRAY
	// this makes it easier to add new (recent) messages
	ArrayList<IMAPMessage> messages;

	// The UID of the first and last downloaded message
	long firstDLUID, lastDLUID;
	
	// The (0-start) indices of the first and last available list items
	// Index into the messages ArrayList
	// firstVI > lastVI because the first page starts at the end of the array
	int firstViewingIndex, lastViewingIndex;

	// variable used to store items per page
	int itemsPerPage;

	// current page number
	int currentPage;

	// total number of pages that would exist
	// Ceiling(total message count / items per page)
	int totalPages;

	// Messages not yet in the downloaded interval
	// Message before = earlier messages; belong at end of messages ArrayList
	// Message after = later messages; belong at beginning of messages ArrayList
	int messagesBefore;
	int messagesAfter;

	/**
	 * Constructor
	 * @param f - the folder containing messages to display
	 * @throws MessagingException - trouble accessing folder
	 * @throws FileNotFoundException - if folder does not exist
	 */
	public MessageListModel(IMAPFolder f) throws MessagingException, FileNotFoundException{
		folder = f;

		// open folder if it is not already open
		try {
			if(folder.exists()){
				if (folder.isOpen()){
					folder.close(false);
					System.out.println("Folder " + folder + " was openned before expected.  This shouldn't happen. " +
					"Closing without expunge and reopenning...");
				}
				folder.open(IMAPFolder.READ_ONLY);  //for testing purposes.  change to READ_WRITE later
			} else {
				throw new FileNotFoundException();
			}
		} catch (MessagingException e1) {
			System.out.println("Exception in FolderController: constructor: current folder " + folder);
			e1.printStackTrace();
		}

		// add listeners that would be notified when in idle mode
		folder.addMessageCountListener(newMessageCountListener());

		// download interval is zero
		// interval is placed in front of all undownloaded messages
		messages = new ArrayList<IMAPMessage>();
		dataListeners = new LinkedList<ListDataListener>();
		messagesBefore = 0;
		messagesAfter = folder.getMessageCount();

		itemsPerPage = ITEMS_PER_PAGE;
		currentPage = 1;

		// initializes the view
		moveView();
	}

	/**
	 * Given a page number and items per page, slide the view down the array, downloading more messages as needed.
	 * @throws MessagingException
	 */
	private synchronized void moveView() throws MessagingException{

		int totalMessages = folder.getMessageCount();

		// if unregistered updates were made, then start over
		// this means if messages were added or removed since last update
		// (shouldn't happen once idle is implemented)
		if( messagesBefore+messagesAfter+messages.size() != totalMessages){
			messagesAfter = totalMessages;
			messagesBefore = 0;
			messages = new ArrayList<IMAPMessage>();
		}

		// if the current page is out of bounds, set it to the nearest page with messages on it
		currentPage = Math.min(currentPage, totalMessages/itemsPerPage);
		currentPage = Math.max(1, currentPage);

		totalPages = (int) Math.ceil((0.0 + totalMessages)/itemsPerPage);

		// reset the viewing indexes
		// firstViewingIndex > lastViewingIndex because we read the messages ArrayList backwards
		firstViewingIndex = totalMessages - (currentPage - 1)*itemsPerPage - 1; // w/o -1 at end it would be the size
		lastViewingIndex = totalMessages - Math.min(totalMessages, (currentPage)*itemsPerPage); // last valid index in 0-array 

		//firstViewingIndex = totalMessages - (currentPage - 1)*itemsPerPage - 1;
		//lastViewingIndex = firstViewingIndex - Math.min(itemsPerPage, totalMessages - itemsPerPage*(currentPage-1));


		// if the first index is more than the downloaded interval, then download as needed
		// remember we reading from the back!  The first page has the newest (highest #) messages.
		if(firstViewingIndex + 1 > messages.size() + messagesAfter){
			// imap folder msg numbers start at 1 and end at size.  all our indexes start at 0 and end at size-1.  compensate for this.
			Message[] messagesArray = folder.getMessages(totalMessages-messagesBefore+1, firstViewingIndex+1);
			LinkedList<IMAPMessage> ll = new LinkedList<IMAPMessage>();
			// print out messages.  useful because UI takes a while to load
			System.out.println("------ Loaded messages: ------");
			for (Message msg: messagesArray){
				ll.add((IMAPMessage)msg);
				System.out.println(msg.getSubject());
			}	
			System.out.println("");

			// add to beginning of downloaded interval (end of backwards-read array)
			messages.addAll(ll);
			messagesBefore = messagesBefore - messagesArray.length;
		}

		// if the last index is less than the downloaded interval, then download as needed
		if (lastViewingIndex < messagesAfter){
			// imap folder msg numbers start at 1.  all our indexes start at 0.  compensate for this.
			Message[] messagesArray = folder.getMessages(lastViewingIndex+1, messagesAfter);
			LinkedList<IMAPMessage> ll = new LinkedList<IMAPMessage>();
			System.out.println("------ Loaded messages: ------");
			for (Message msg: messagesArray){
				ll.add((IMAPMessage)msg);

				System.out.println(msg.getSubject());
			}	
			System.out.println("");

			// add to end of downloaded interval (front of backwards-read array)
			messages.addAll(0, ll);
			messagesAfter = messagesAfter - messagesArray.length;
		}

		//System.out.println("First VI: " + firstViewingIndex);
		//System.out.println("Last VI: " + lastViewingIndex);

		//System.out.println("messages.size(): " + messages.size());
		//System.out.println("getSize(): " + getSize());

	}

	/**
	 * Listeners for data changes
	 * JList is the intended user of this interface
	 */
	@Override
	public void addListDataListener(ListDataListener listener) {
		dataListeners.add(listener);
		//System.out.println("Added");
		listener.intervalAdded(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize()));
	}

	/**
	 * JList is the intended user of this interface
	 */
	@Override
	public Object getElementAt(int viewingIndex) {
		if ((viewingIndex < 0) || (viewingIndex > (getSize() - 1))){
			throw new IndexOutOfBoundsException( "" + viewingIndex + " is outside the range 0 - " + getSize());
		}
		return messages.get(firstViewingIndex-viewingIndex-messagesAfter);
	}

	/**
	 * JList is the intended user of this interface
	 */
	@Override
	public int getSize() {
		return firstViewingIndex-lastViewingIndex+1;
	}

	/**
	 * Listeners for data changes
	 * JList is the intended user of this interface
	 */
	@Override
	public void removeListDataListener(ListDataListener listener) {
		dataListeners.remove(listener);
	}

	/**
	 * Change the number of items displayed per page (i.e. view)
	 * @param i items to display per page
	 * @throws MessagingException
	 */
	public void setItemsPerPage(int i) throws MessagingException{
		itemsPerPage = i;
		update();
	}

	/**
	 * Change view to the next page if there is a next page
	 * @return - true if there is a next page after operation completes
	 * @throws MessagingException
	 */
	public boolean next () throws MessagingException{
		currentPage++;
		update();
		return hasNext();	
	}

	/**
	 * Change view to the previous page if there is a previous page
	 * @return - true if there is a previous page after operation completes
	 * @throws MessagingException
	 */
	public boolean prev () throws MessagingException{
		currentPage--;
		update();
		return hasPrev();	
	}

	/**
	 * Ask whether a previous page exists
	 * @return - true if a previous page exists
	 */
	public boolean hasPrev(){
		boolean hasNext = false;
		try {
			if((messagesBefore + firstViewingIndex + 1) < folder.getMessageCount()){
				hasNext = true;
			}
		} catch (MessagingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return hasNext;
	}

	/**
	 * Ask whether a next page exists
	 * @return - true if a next page exists
	 */
	public boolean hasNext(){
		boolean hasNext = false;
		if((messagesAfter + lastViewingIndex) > 0){
			hasNext = true;
		}
		return hasNext;
	}

	/**
	 * 
	 * @return the current page
	 */
	public int getPage(){
		return currentPage;
	}

	/**
	 * 
	 * @return total number of pages to display from the data in this folder
	 */
	public int getTotalPages(){
		return totalPages;
	}

	/**
	 * Not implemented or tested
	 * 
	 * @return Listener for requests to expunge a set of messages
	 */
	public ActionListener getExpungeSelectedListener (){
		if (expungeAction == null){
			expungeAction = new ActionListener(){

				@Override
				public void actionPerformed(ActionEvent e) {
					IMAPMessage[] selected = (IMAPMessage[]) e.getSource();

					/* THIS IS WHERE MOVE TO TRASH FOLDER WOULD GO.  Not currently implemented */


					List<IMAPMessage> sublist = messages.subList(firstViewingIndex, lastViewingIndex);
					LinkedList<IMAPMessage> toRemove = new LinkedList<IMAPMessage>();

					for(IMAPMessage message: selected){						
						if(sublist.contains(message)){
							toRemove.add(message);

							try {
								message.setFlag(Flag.DELETED, true);
							} catch (MessagingException e1) {
								System.out.println("ExpungeSelectedListener: Exception setting flag to delete of message: " + message);
								e1.printStackTrace();
							}
						}
					}
					// this will generate a MessageCountEvent displayed to MessageCountListeners
					try {
						folder.expunge(selected);
						messages.remove(toRemove);
						update();
					} catch (MessagingException e1) {
						System.out.println("FolderController: getExpungeSelectedListener: Exception deleting messages: " + messages);
						e1.printStackTrace();
					}
				}

			};
		}
		return expungeAction;	
	}

	/**
	 * Not implemented or tested
	 * 
	 * @return - Listener for requests to mark a set of messages as unread
	 */
	public ActionListener getMarkUnseenSelectedListener (){
		if (markUnseenAction == null){
			markUnseenAction = new ActionListener(){

				@Override
				public void actionPerformed(ActionEvent e) {
					IMAPMessage[] selected = (IMAPMessage[]) e.getSource();


					List<IMAPMessage> sublist = messages.subList(firstViewingIndex, lastViewingIndex);

					for(IMAPMessage message: selected){				
						if(sublist.contains(message)){

							try {
								message.setFlag(Flag.SEEN, false);
							} catch (MessagingException e1) {
								System.out.println("MarkUnseenSelectedListener: Exception setting flag to delete of message: " + message);
								e1.printStackTrace();
							}
						}
					}

					update();
				}

			};
		}
		return markUnseenAction;	
	}

	/**
	 * Moves the viewing list to the interval of messages described by page number and items per page
	 * 
	 * Then notifies listeners that the list's data has changed
	 */
	private void update(){
		try {
			moveView();

			ListDataEvent lde = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize());
			for(ListDataListener listener: dataListeners){
				listener.intervalAdded(lde);
			}
		} catch (MessagingException e1) {
			System.out.println("MoveView Exception.\n" + e1.getLocalizedMessage());
			e1.printStackTrace();
		}
	}

	/**
	 * Creates a message count listener to listen for added and removed message notifications while in an idle state
	 * 
	 * Not tested or implemented.
	 * 
	 * @return
	 */
	private MessageCountListener newMessageCountListener(){
		return new MessageCountListener(){

			Comparator<IMAPMessage> messageComparator = new Comparator<IMAPMessage>(){

				/**
				 * 
				 * @param mes1
				 * @param mes2
				 * @return Negative value, 0, or positive value indicating
				 * whether mes1 is less than, equal to, to greater than mes2
				 */
				@Override
				public int compare(IMAPMessage mes1, IMAPMessage mes2) {
					int val = 0;
					try {
						val = ((int)(folder.getUID(mes1) - folder.getUID(mes2)));
					} catch (MessagingException e) {
						System.out.println("Exception in FolderController: initializeList: could not get message UID to compare.\n" +
								"Message 1: " + mes1 + "\n" +
								"Message 2: " + mes1);
						e.printStackTrace();
					}
					return val;
				}

			};

			@Override
			public synchronized void messagesAdded(MessageCountEvent e) {
				boolean update = false;
				long firstUID = 0;
				long lastUID = 0;
				long lastViewingUID = 0;

				// adds the message to the messages ArrayList at a location determined by its UID
				// UID are always in chronological order
				
				// if the UID is before the UID of the last viewing index (i.e. if the message is dropped
				// in before or on the current page), then it shifts the viewed messages and the view needs to be updated
				try{
					firstUID = folder.getUID(messages.get(messages.size()));
					lastUID = folder.getUID(messages.get(0));
					lastViewingUID = folder.getUID(messages.get(lastViewingIndex));
					System.out.println("Adding messages....");
				} catch (MessagingException e1){
					System.out.println("Error getting UID of message  from folder.\n" + e1.getLocalizedMessage());
					e1.printStackTrace();
				}

				IMAPMessage[] messagesArray = (IMAPMessage[])(e.getMessages());
				System.out.println("" + messagesArray.length + " messages recieved");

				for (IMAPMessage msg: messagesArray){
					long UID = 0;
					try {
						UID = folder.getUID(msg);
					} catch (MessagingException e1) {
						System.out.println("Error getting UID of message  from folder.\n" + e1.getLocalizedMessage());
						e1.printStackTrace();
					}
					if(UID < lastUID){
						messagesAfter++;
					} else if (UID > firstUID){
						messagesBefore++;
					} else {
						int search = Collections.binarySearch(messages, msg, messageComparator);
						// if this message does not already exist (it shouldn't!), then add it to messages
						if(search < 0) {
							// inserts the element at this index
							// search = (-(insertion point) - 1), so insertion point = (-(search) - 1)
							messages.add((-search - 1), msg); 
						}
					}

					if(UID > lastViewingUID){
						update = true;
					}
				}

				if (update){
					update();
				}
			}

			@Override
			public synchronized void messagesRemoved(MessageCountEvent e) {
				boolean update = false;
				long firstUID = 0;
				long lastUID = 0;
				long lastViewingUID = 0;
				
				// removes the message from the messages ArrayList at a location determined by its UID
				// UID are always in chronological order

				try{
					firstUID = folder.getUID(messages.get(messages.size()));
					lastUID = folder.getUID(messages.get(0));
					lastViewingUID = folder.getUID(messages.get(lastViewingIndex));
				} catch (MessagingException e1){
					System.out.println("Error getting UID of message  from folder.\n" + e1.getLocalizedMessage());
					e1.printStackTrace();
				}

				IMAPMessage[] messagesArray = (IMAPMessage[])(e.getMessages());

				for (IMAPMessage msg: messagesArray){
					long UID = 0;
					try {
						UID = folder.getUID(msg);
					} catch (MessagingException e1) {
						System.out.println("Error getting UID of message  from folder.\n" + e1.getLocalizedMessage());
						e1.printStackTrace();
					}
					if(UID < lastUID){
						messagesAfter--;
					} else if (UID > firstUID){
						messagesBefore--;
					} else {
						int index = Collections.binarySearch(messages, msg, messageComparator);
						// if this message does not already exist (it shouldn't!), then add it to messages
						if(index >= 0) {
							messages.remove(index); 
						} else {
							System.out.println("This message mysteriously dissappeared!: " + msg);
						}
					}

					if(UID > lastViewingUID){
						update = true;
					}
				}

				if (update){
					update();
				}
			}
		};
	}
}
